// Copyright (c) 2021 Presto Labs Pte. Ltd.
// Author: hyuan

#include "coin2/exchange/phemex/api_order/native_private_client.h"

#include <memory>
#include <sstream>

#include "coin2/exchange/base/api_util/url_params_builder.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/phemex/symbology/product.h"

using coin2::exchange::phemex::symbology::PhemexProduct;
using AsioHttpContextList = std::list<std::shared_ptr<AsioHttpContext>>;

#define CHECK_API_LIMIT(action_type)                                                      \
  if (!HasEnoughRateLimit(ts, action_type)) {                                             \
    LOG_EVERY_N(WARNING, 50) << "Hit rate limit or api temporary disabled, " << __func__; \
    api_block_info_[__func__]++;                                                          \
    return {};                                                                            \
  }

namespace {
using ActionType = phemex::api_util::PhemexRateTracker::ActionType;
}

auto PhemexNativePrivateClient::GetRestOrderParams(
    const OrderSpec& order_spec,
    int64_t proc_order_id) -> RestParams {
  auto params = RestParams();

  PhemexProduct product = PhemexProduct::FromStr(order_spec.product, order_spec.order_created_time);
  params.set_symbol(order_spec.product);
  params.set_client_order_id(std::to_string(proc_order_id));
  if (order_spec.order_side == OrderSide::BUY_ORDER) {
    params.set_side("Buy");
  } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
    params.set_side("Sell");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
  }
  params.set_qty_type("ByBase");
  params.set_base_qty_ev(std::llround(order_spec.qty * 1e8));
  if (order_spec.order_type != OrderType::MARKET_ORDER) {
    params.set_price_ep(std::llround(order_spec.price * 1e8));
  }
  if (order_spec.order_type == OrderType::MARKET_ORDER) {
    params.set_ord_type("Market");
  } else {
    params.set_ord_type("Limit");
    if (order_spec.post_only) {
      params.set_time_in_force("PostOnly");
    } else if (order_spec.duration == coin::proto::OrderDuration::FOK_ORDER) {
      params.set_time_in_force("FillOrKill");
    } else if (order_spec.duration == coin::proto::OrderDuration::IOC_ORDER) {
      params.set_time_in_force("ImmediateOrCancel");
    } else {
      params.set_time_in_force("GoodTillCancel");
    }
  }

  return params;
}

auto PhemexNativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  auto params = RestParams();
  const std::string& product_str = order_context.order_spec().product;
  params.set_symbol(product_str);
  if (order_context.has_exchange_order_id()) {
    params.set_order_id(order_context.exchange_order_id());
  } else {
    params.set_client_order_id(std::to_string(order_context.proc_order_id()));
  }

  return params;
}

PhemexNativePrivateClient::PhemexNativePrivateClient(
    const MarketExchangeApi& mea,
    const NativeClientConfig& config_override) {
  mea_ = mea;
  NativeClientConfig config;
  config.set_rest_host("api.phemex.com");
  config.set_rest_host_port(443);
  config.MergeFrom(config_override);
  LOG(INFO) << "[PhemexNativePrivateClient] config: " << config.DebugString();

  auths_.Construct<PhemexRestAuth, NativeClientConfig>(config);

  rate_tracker_ = std::make_unique<PhemexRateTracker>(mea_, config);
  local_rate_tracker_ = static_cast<PhemexRateTracker*>(rate_tracker_.get());
  start_time_ = GetCurrentTimestamp();
}

std::shared_ptr<AsioHttpContext> PhemexNativePrivateClient::QueryAccountBalance(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::UNKNOWN);

  UrlParamsBuilder builder;
  builder.PutGet("currency", params.currency());
  auto context = std::shared_ptr<AsioHttpContext>(
      GetAuth()->CreateRequestByGetWithSignature("/spot/wallets", &builder));
  Push(1, ts, ActionType::UNKNOWN);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> PhemexNativePrivateClient::QueryOpenOrders(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY);

  UrlParamsBuilder builder;
  builder.PutGet("symbol", params.symbol());
  auto context = std::shared_ptr<AsioHttpContext>(
      GetAuth()->CreateRequestByGetWithSignature("/spot/orders", &builder));
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> PhemexNativePrivateClient::QueryHistoryOrders(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::UNKNOWN);

  UrlParamsBuilder builder;
  builder.PutGet("symbol", params.symbol());
  builder.PutGet("limit", 200);
  auto context = std::shared_ptr<AsioHttpContext>(
      GetAuth()->CreateRequestByGetWithSignature("/api-data/spots/orders", &builder));
  Push(1, ts, ActionType::UNKNOWN);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_DONE_ORDERS;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> PhemexNativePrivateClient::QueryFills(const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::UNKNOWN);

  UrlParamsBuilder builder;
  builder.PutGet("symbol", params.symbol());
  builder.PutGet("limit", 200);
  auto context = std::shared_ptr<AsioHttpContext>(
      GetAuth()->CreateRequestByGetWithSignature("/api-data/spots/trades", &builder));
  Push(1, ts, ActionType::UNKNOWN);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> PhemexNativePrivateClient::QueryDeposit(const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::UNKNOWN);

  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByGetWithSignature(
      "/phemex-deposit/wallets/api/depositHist",
      &builder);
  Push(1, ts, ActionType::UNKNOWN);
  api_req_info_[__func__]++;

  std::string extra_info = std::string("Deposit") + params.currency();
  context->query_type = PhemexOrderSubTopicId::REST_DEPOSIT;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> PhemexNativePrivateClient::QueryWithdraw(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::UNKNOWN);

  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByGetWithSignature(
      "/phemex-withdraw/wallets/api/withdrawHist",
      &builder);
  Push(1, ts, ActionType::UNKNOWN);
  api_req_info_[__func__]++;

  context->query_type = PhemexOrderSubTopicId::REST_WITHDRAWAL;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> PhemexNativePrivateClient::QuerySpotAccountTransfer(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::UNKNOWN);

  UrlParamsBuilder builder;
  builder.PutGet("currency", params.currency());
  builder.PutGet("limit", 200);

  if (params.has_start_time() && params.has_end_time()) {
    builder.PutGet("start", params.start_time());
    builder.PutGet("end", params.end_time());
  }

  std::string extra_info = std::string("SpotAccountTransfer") + params.currency();
  auto* context =
      GetAuth()->CreateRequestByGetWithSignature("/assets/spots/sub-accounts/transfer", &builder);
  Push(1, ts, ActionType::UNKNOWN);
  api_req_info_[__func__]++;

  context->query_type = PhemexOrderSubTopicId::TRANSFER_SPOT;
  context->extra_info = extra_info;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> PhemexNativePrivateClient::QueryFuturesAccountTransfer(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::UNKNOWN);

  UrlParamsBuilder builder;
  builder.PutGet("currency", params.currency());
  builder.PutGet("limit", 200);

  if (params.has_start_time() && params.has_end_time()) {
    builder.PutGet("start", params.start_time());
    builder.PutGet("end", params.end_time());
  }

  std::string extra_info = std::string("FuturesAccountTransfer") + params.currency();
  auto* context =
      GetAuth()->CreateRequestByGetWithSignature("/assets/futures/sub-accounts/transfer", &builder);
  Push(1, ts, ActionType::UNKNOWN);
  api_req_info_[__func__]++;

  context->query_type = PhemexOrderSubTopicId::TRANSFER_FUTURES;
  context->extra_info = extra_info;
  return std::shared_ptr<AsioHttpContext>(context);
}

void PhemexNativePrivateClient::Prepare() {
  using coin2::exchange::phemex::api_util::PhemexRateTracker;
  auto* tracker = dynamic_cast<PhemexRateTracker*>(this->rate_tracker_.get());
  mb_->Subscribe("phemex/rate_limit_spotorder", [tracker](const std::string&, void* data) {
    int64_t rate_limit_remain = reinterpret_cast<int64_t>(data);
    tracker->UpdateRateLimitSpotOrder(rate_limit_remain);
  });

  mb_->Subscribe("phemex/enable_time_spotorder", [this](const std::string&, void* data) {
    int64_t value = reinterpret_cast<int64_t>(data);
    local_rate_tracker_->UpdateSpotOrderEnableTime(value);
    LOG(WARNING) << "Phemex native client enable_time for spotOrder: " << value;
  });

  mb_->Subscribe("phemex/rate_limit_others", [tracker](const std::string&, void* data) {
    int64_t rate_limit_remain = reinterpret_cast<int64_t>(data);
    tracker->UpdateRateLimitOthers(rate_limit_remain);
  });

  mb_->Subscribe("phemex/enable_time_others", [this](const std::string&, void* data) {
    int64_t value = reinterpret_cast<int64_t>(data);
    local_rate_tracker_->UpdateOthersEnableTime(value);
    LOG(WARNING) << "Phemex native client enable_time for others: " << value;
  });

  mb_->Subscribe("phemex/user_id", [this](const std::string&, void* data) {
    this->user_id = *reinterpret_cast<std::string*>(data);
  });
}

std::string PhemexNativePrivateClient::DebugString() const {
  double delta_t = (GetCurrentTimestamp() - start_time_) / 1.0e9;
  std::stringstream ss;
  ss << "PhemexNativePrivateClient::DebugString\n";
  ss << "api info\n";
  for (auto& [name, value] : api_req_info_) {
    int32_t blocked = 0;
    if (api_block_info_.count(name) > 0) {
      blocked = api_block_info_.at(name);
    }
    int32_t total_req = value + blocked;
    ss << name << " (total:" << total_req << ", succeeded: " << value << ", blocked: " << blocked
       << ", req_rate: " << value / delta_t << ", block_rate: " << blocked / delta_t << ")\n";
  }
  return ss.str();
}

std::shared_ptr<AsioHttpContext> PhemexNativePrivateClient::PlaceOrder(const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::PLACE);

  UrlParamsBuilder builder;
  builder.PutPost("symbol", params.symbol());
  builder.PutPost("clOrdID", params.client_order_id());
  builder.PutPost("side", params.side());
  builder.PutPost("qtyType", params.qty_type());
  builder.PutPost("baseQtyEv", params.base_qty_ev());
  builder.PutPost("priceEp", params.price_ep());
  builder.PutPost("ordType", params.ord_type());
  builder.PutPost("timeInForce", params.time_in_force());
  auto context = std::shared_ptr<AsioHttpContext>(
      GetAuth()->CreateRequestByPostWithSignature("/spot/orders", &builder));
  Push(1, ts, ActionType::PLACE);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> PhemexNativePrivateClient::CancelOrder(const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::CANCEL);

  UrlParamsBuilder builder;
  builder.PutGet("symbol", params.symbol());
  if (params.has_order_id()) {
    builder.PutGet("orderID", params.order_id());
  } else if (params.has_client_order_id()) {
    builder.PutGet("clOrdID", params.client_order_id());
  }
  auto context = std::shared_ptr<AsioHttpContext>(
      GetAuth()->CreateRequestByDeleteWithSignature("/spot/orders", &builder));
  Push(2, ts, ActionType::CANCEL);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
  context->extra_info = DebugString();
  return context;
}

std::shared_ptr<AsioHttpContext> PhemexNativePrivateClient::PlaceOrderNorm(
    const OrderExecution& order_exe) {
  const auto& order_spec = *order_exe.order_spec;
  const auto proc_order_id = order_exe.proc_order_id;
  auto params = GetRestOrderParams(order_spec, proc_order_id);
  return PlaceOrder(params);
}

std::shared_ptr<AsioHttpContext> PhemexNativePrivateClient::CancelOrderNorm(
    const OrderExecution& order_exe) {
  auto params = GetRestCancelParams(*order_exe.oc);
  return CancelOrder(params);
}

std::shared_ptr<AsioHttpContext> PhemexNativePrivateClient::SubmitOrderExecution(
    const OrderExecutionProto& order) {
  switch (order.type()) {
    case OrderExecutionProto::SUBMIT_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return PlaceOrder(params);
    }
    case OrderExecutionProto::CANCEL_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return CancelOrder(params);
    }
    default: {
      return nullptr;
    }
  }
}

std::shared_ptr<AsioHttpContext> PhemexNativePrivateClient::SubmitRestQuery(
    const RestQueryProto& query) {
  RestParams params;
  switch (query.type()) {
    case RestQueryProto::QUERY_ACCOUNT_BALANCE: {
      params.set_currency("BTC");
      return QueryAccountBalance(params);
    }
    case RestQueryProto::QUERY_OPEN_ORDERS: {
      params.set_symbol("sBTCUSDT");
      return QueryOpenOrders(params);
    }
    default: {
      return nullptr;
    }
  }
}

AsioHttpContextList PhemexNativePrivateClient::SubmitRestQueryList(const RestQueryProto& query) {
  AsioHttpContextList list;
  RestParams params;
  const auto& currency_list = GetTransferCurrencyList();
  switch (query.type()) {
    case RestQueryProto::QUERY_TRANSFERS: {
      for (auto currency : currency_list) {
        params.set_start_time(query.start_time() / 1000'000);
        params.set_end_time(query.end_time() / 1000'000);
        params.set_currency(currency);
        list.emplace_back(QuerySpotAccountTransfer(params));
        list.emplace_back(QueryFuturesAccountTransfer(params));
      }
      break;
    }
    case RestQueryProto::QUERY_DEPOSIT: {
      list.emplace_back(QueryDeposit(params));
      break;
    }
    case RestQueryProto::QUERY_WITHDRAW: {
      list.emplace_back(QueryWithdraw(params));
      break;
    }
    default: {
      return list;
    }
  }

  return list;
}